//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkScripts
#include <NilkinsScripts/Log/LogManager.h>

#include <NilkinsScripts/Environments/Functions/Function.h>

#include <NilkinsScripts/Environments/UserTypes/ArrayAccessorDescriptor.h>
#include <NilkinsScripts/Environments/UserTypes/UserTypeFieldDescriptor.h>
#include <NilkinsScripts/Environments/UserTypes/UserType.h>

#include <NilkinsScripts/Environments/Environment.h>
#include <NilkinsScripts/Environments/EnvironmentManager.h>

#include <NilkinsScripts/Interpreters/Interpreter.h>

#include <NilkinsScripts/Scripts/Script.h>
#include <NilkinsScripts/Scripts/ScriptManager.h>

// Standards
#include <atomic>
#include <memory>
#include <thread>

/// Structures -------------------------------

// Class used as a reference for our user type
class Data
{
	public :

		std::string _label = "I" ;
		int _i = 0 ;
} ;

/// Functions --------------------------------

nkScripts::UserType* prepareDataUserType (nkScripts::Environment* env)
{
	// Create the user type
	nkScripts::UserType* type = env->setUserType("nkTutorial::Data") ;

	// Make its constructor and destructor
	type->setConstructor
	(
		[] (const nkScripts::DataStack& stack) -> void*
		{
			return new Data () ;
		}
	) ;

	type->setDestructor
	(
		[] (void* data) -> void
		{
			delete (Data*)data ;
		}
	) ;

	// Type is ready for usage
	return type ;
}

void demonstrateThreadingSimple (nkScripts::Environment* env)
{
	// Prepare what our main thread will want to communicate to the off thread which is now waiting
	nkScripts::Script script ;
	script.setTargetInterpreter(nkScripts::INTERPRETER::LUA) ;

	script.setSources
	(
		R"eos(
			data = {} ;
			data._processed = false ;
			data._counter = 0 ;
		)eos"
	) ;
	script.load() ;

	env->execute(script) ;

	// Now we retrieve the reference over the object to serialize it
	nkScripts::ScriptObjectReference objRef = env->getVar("data") ;
	nkMemory::Buffer dataSerialized = env->serializeScriptObject(objRef) ;

	// Prepare the threading values
	std::atomic<bool> offThreadProcessing (false) ;

	// Create the thread that will process data
	// This part will run off our main thrad and is totally isolated
	// This is because environment can only run in one thread at a time
	std::thread offThread
	(
		[&offThreadProcessing, &dataSerialized] ()
		{
			// Wait for work to get there
			while (!offThreadProcessing)
				std::this_thread::sleep_for(std::chrono::milliseconds(100)) ;

			// Our environment is created now, but in practice it could be already allocated and ready
			// We allocate it in this scope only, so we avoid the manager, which is non thread safe
			nkMemory::UniquePtr<nkScripts::Environment> offThreadEnv = nkScripts::Environment::create() ;
			offThreadEnv->setEnvironmentFor(nkScripts::INTERPRETER::LUA) ;

			// Serialized data is ready for unpack
			nkScripts::ScriptObjectReference objectRef = offThreadEnv->deserializeScriptObject(dataSerialized) ;
			// At that point, the data is not assigned to any variable within the environment
			// We will assign it to a variable for it to be accessible in the environment
			// This is done so that user can choose where to assign the data and avoid name clashing anything
			offThreadEnv->setVar("data", objectRef) ;

			// Now to run the script
			// We will create it for this scope only, don't use the Manager (which is also not thread safe)
			nkScripts::Script script ;
			script.setTargetInterpreter(nkScripts::INTERPRETER::LUA) ;

			script.setSources
			(
				R"eos(
					-- Simple processing here, but high load could be done without blocking the main thread
					data._processed = true ;
					data._counter = 42 ;
				)eos"
			) ;
			script.load() ;

			offThreadEnv->execute(script) ;

			// Now that our object has been processed, we will update its serialization
			dataSerialized = offThreadEnv->serializeScriptObject(objectRef) ;

			// Reset the processing flag
			offThreadProcessing = false ;
		}
	) ;

	// Request the thread to process the data
	offThreadProcessing = true ;

	// And wait till it's good
	// In practice, we would just do our own processing in UI / rendering, to avoid locking
	while (offThreadProcessing)
		std::this_thread::sleep_for(std::chrono::milliseconds(100)) ;

	// Data has been updated, let's pass it to our environment
	// Note that we need to reassign it : it is in fact a new object instance that has been deserialized
	objRef = env->deserializeScriptObject(dataSerialized) ;
	env->setVar("data", objRef) ;

	// Test in the script to see what's happening
	script.unload() ;
	script.setSources
	(
		R"eos(
			print("Object : " .. tostring(data._processed) .. " -> " .. data._counter) ;
		)eos"
	) ;
	script.load() ;

	env->execute(script) ;

	// Don't forget to join before going out of scope
	if (offThread.joinable())
		offThread.join() ;

	// Using this serialization mechanism, it is possible to pass around data from one environment to another easily
	// This is required because environments have their own memory space
	// In theory, objects could also be set using the environment API (setVar, setObject...)
	// But serialization can handle opaque objects like this object defined in Lua
}

void demonstrateThreadingWithCppObject (nkScripts::Environment* env)
{
	// Prepare the threading values
	std::atomic<bool> offThreadProcessing (false) ;
	nkMemory::Buffer dataSerialized ;

	// Create the thread that will process data
	// This part will run off our main thrad and is totally isolated
	// This is because environment can only run in one thread at a time
	std::thread offThread
	(
		[&offThreadProcessing, &dataSerialized] ()
		{
			// Wait for work to get there
			while (!offThreadProcessing)
				std::this_thread::sleep_for(std::chrono::milliseconds(100)) ;

			// Our environment is created now, but in practice it could be already allocated and ready
			// We allocate it in this scope only, so we avoid the manager, which is non thread safe
			nkMemory::UniquePtr<nkScripts::Environment> env = nkScripts::Environment::create() ;
			env->setEnvironmentFor(nkScripts::INTERPRETER::LUA) ;

			nkScripts::UserType* type = prepareDataUserType(env.get()) ;

			// Serialized data is ready for unpack
			nkScripts::ScriptObjectReference objectRef = env->deserializeScriptObject(dataSerialized) ;
			// At that point, the data is not assigned to any variable within the environment
			// We will assign it to a variable for it to be accessible in the environment
			// This is done so that user can choose where to assign the data and avoid name clashing anything
			env->setVar("data", objectRef) ;

			// Now to run the script
			// We will create it for this scope only, don't use the Manager (which is also not thread safe)
			nkScripts::Script script ;
			script.setTargetInterpreter(nkScripts::INTERPRETER::LUA) ;

			script.setSources
			(
				R"eos(
					-- Simple processing here, but high load could be done without blocking the main thread
					data._processed = true ;
					data._counter = 42 ;

					-- This time, also add a new allocation of a Cpp object
					data._object = nkTutorial.Data.new() ;
				)eos"
			) ;
			script.load() ;

			env->execute(script) ;

			// Now that our object has been processed, we will update its serialization
			// This time, we forward object ownership, meaning that the Data object allocated inside will be considered as owned by environments deserializing the buffer
			dataSerialized = env->serializeScriptObject(objectRef, true) ;

			// Reset the processing flag
			offThreadProcessing = false ;
		}
	) ;

	// Prepare what our main thread will want to communicate to the off thread which is now waiting
	nkScripts::Script script ;
	script.setTargetInterpreter(nkScripts::INTERPRETER::LUA) ;

	script.setSources
	(
		R"eos(
			data = {} ;
			data._processed = false ;
			data._counter = 0 ;
		)eos"
	) ;
	script.load() ;

	env->execute(script) ;

	// Now we retrieve the reference over the object to serialize it
	nkScripts::ScriptObjectReference objRef = env->getVar("data") ;
	dataSerialized = env->serializeScriptObject(objRef) ;

	// Request the thread to process the data
	offThreadProcessing = true ;

	// And wait till it's good
	// In practice, we would just do our own processing in UI / rendering, to avoid locking
	while (offThreadProcessing)
		std::this_thread::sleep_for(std::chrono::milliseconds(100)) ;

	// Data has been updated, let's pass it to our environment
	// Note that we need to reassign it : it is in fact a new object instance that has been deserialized
	objRef = env->deserializeScriptObject(dataSerialized) ;
	env->setVar("data", objRef) ;

	// Test in the script to see what's happening
	script.unload() ;
	script.setSources
	(
		R"eos(
			print("Object : " .. tostring(data._processed) .. " -> " .. data._counter .. " - " .. tostring(data._object)) ;
		)eos"
	) ;
	script.load() ;

	env->execute(script) ;

	// Don't forget to join before going out of scope
	if (offThread.joinable())
		offThread.join() ;

	// Forwarding ownership can be very important to get your algorithms right
	// If the processing thread garbage collects the object passed back, then the C pointer will be invalid
	// By default, ownership is not forwarded, meaning the creating thread is still considered as the owner after serialization
}

/// Main -------------------------------------

int main ()
{
	// Prepare for logging
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkScripts::LogManager::getInstance()->setReceiver(logger.get()) ;

	// Create our environment
	nkScripts::Environment* env = nkScripts::EnvironmentManager::getInstance()->createOrRetrieve("firstEnv") ;
	env->setEnvironmentFor(nkScripts::INTERPRETER::LUA) ;

	// Basis for the user type
	nkScripts::UserType* type = prepareDataUserType(env) ;

	demonstrateThreadingSimple(env) ;
	demonstrateThreadingWithCppObject(env) ;

	// Small pause to be able to witness the console
	system("pause") ;

	return 0 ;
}